Системное программирование

Тема 3. Объекты ядра и их использование в приложении

Системное программирование

План лекции

1. Предупреждение состояния состязаний

2. Средства взаимного исключения

3. Синхронизация потоков

4. Создание и закрытие процессов и потоков

5. Открытие и закрытие файлов

6. Мьютексы

7. Семафоры

Объекты ядра и их использование в приложении
Системное программирование

1. Предупреждение состояния состязаний

1.1. Проблема состояния гонки

Состояние гонки (Race Condition) — ситуация, когда результат выполнения программы зависит от порядка выполнения операций в разных потоках.

Пример:

// Общая переменная
int counter = 0;

// Поток 1: counter++    // Поток 2: counter++
MOV EAX, [counter]       MOV EAX, [counter]
INC EAX                  INC EAX
MOV [counter], EAX       MOV [counter], EAX
// Результат: 1 вместо 2!

Критическая секция — участок кода, обращающийся к разделяемым данным.

Объекты ядра и их использование в приложении
Системное программирование

1.2. Четыре условия корректности критической секции

Любое решение задачи критической секции должно обеспечивать выполнение четырёх условий:

1. Взаимное исключение (Mutual Exclusion)

Если поток PiP_i выполняет свою критическую секцию, то никакой другой поток PjPiP_j \neq P_i не может выполнять свою критическую секцию.

2. Прогресс (Progress)

Если ни один поток не находится в критической секции и несколько потоков желают войти в неё, то решение о том, какой поток войдёт следующим, может приниматься только среди этих потоков. Выбор не может быть отложен бесконечно.

3. Ограниченное ожидание (Bounded Waiting)

Существует граница NN на число раз, которое другие потоки могут войти в критическую секцию после того, как поток выразил желание войти, и до того, как это разрешение будет ему предоставлено.

4. Нет предположений о скорости

Не делается никаких предположений об относительной скорости потоков или количестве процессоров.

Объекты ядра и их использование в приложении
Системное программирование

Классификация решений:

  • Аппаратные (атомарные инструкции)
  • Программные (алгоритмы Петерсона, Деккера)
  • Средства ОС (объекты ядра: мьютексы, семафоры)
Объекты ядра и их использование в приложении
Системное программирование

2. Средства взаимного исключения

2.1. Аппаратные средства

Атомарные операции:

// Test-And-Set
int test_and_set(int *lock) {
    int old = *lock;
    *lock = 1;
    return old;
}

// Compare-And-Swap (CAS)
int compare_and_swap(int *value, int expected, int new_value) {
    int temp = *value;
    if (*value == expected)
        *value = new_value;
    return temp;
}

Инструкции процессора:

  • xchg — атомарный обмен
  • lock prefix — блокировка шины
  • cmpxchg — сравнение и обмен
Объекты ядра и их использование в приложении
Системное программирование

2.2. Спинлоки

Спинлок — механизм активного ожидания.

volatile int lock = 0;

void acquire() {
    while (test_and_set(&lock) == 1)
        ; // Активное ожидание
}

void release() {
    lock = 0;
}

Применение:

  • Короткие критические секции
  • Многопроцессорные системы
  • Ядро ОС

Недостатки:

  • Занятие процессора
  • Неэффективен при высокой конкуренции
Объекты ядра и их использование в приложении
Системное программирование

3. Синхронизация потоков

3.1. Объекты синхронизации Windows

Типы объектов ядра:

Объект Назначение
Mutex Взаимное исключение
Semaphore Ограничение доступа к ресурсу
Event Уведомление о событии
Critical Section Быстрое взаимное исключение
Waitable Timer Ожидание по времени

Состояния объектов:

  • Signaled — свободен, поток может продолжить
  • Nonsignaled — занят, поток блокируется
Объекты ядра и их использование в приложении
Системное программирование

3.2. Функции ожидания Windows

// Ожидание одного объекта
DWORD WaitForSingleObject(
    HANDLE hHandle,
    DWORD dwMilliseconds
);

// Ожидание нескольких объектов
DWORD WaitForMultipleObjects(
    DWORD nCount,
    const HANDLE *lpHandles,
    BOOL bWaitAll,
    DWORD dwMilliseconds
);

// Константы времени ожидания
INFINITE      // Бесконечное ожидание
0             // Проверка без блокировки
Объекты ядра и их использование в приложении
Системное программирование

4. Создание и закрытие процессов и потоков

4.1. Создание процесса в Windows

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;

BOOL result = CreateProcess(
    L"C:\\Windows\\notepad.exe",
    NULL, NULL, NULL, FALSE,
    CREATE_NEW_CONSOLE,
    NULL, NULL,
    &si, &pi
);

if (result) {
    // pi.hProcess — дескриптор процесса
    // pi.hThread — дескриптор первичного потока
    // pi.dwProcessId — идентификатор процесса
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}
Объекты ядра и их использование в приложении
Системное программирование

4.2. Создание потока в Windows

DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    int* data = (int*)lpParam;
    // Работа потока
    return 0;
}

HANDLE hThread = CreateThread(
    NULL,           // Атрибуты безопасности
    0,              // Размер стека
    ThreadFunc,     // Функция потока
    &param,         // Параметр
    0,              // Флаги
    &threadId       // ID потока
);

WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
Объекты ядра и их использование в приложении
Системное программирование

4.3. Завершение процессов и потоков

Нормальное завершение:

// Поток
return value;           // Из функции потока
ExitThread(exitCode);   // Явное завершение

// Процесс
ExitProcess(exitCode);  // Завершение процесса

Принудительное завершение:

// Осторожно! Может привести к утечкам
TerminateThread(hThread, exitCode);
TerminateProcess(hProcess, exitCode);

Проблемы принудительного завершения:

  • Не освобождаются ресурсы
  • Не вызываются деструкторы
  • Не снимаются блокировки
Объекты ядра и их использование в приложении
Системное программирование

5. Открытие и закрытие файлов

5.1. Работа с файлами в Windows

HANDLE CreateFile(
    LPCTSTR lpFileName,
    DWORD dwDesiredAccess,      // GENERIC_READ, GENERIC_WRITE
    DWORD dwShareMode,          // FILE_SHARE_READ, FILE_SHARE_WRITE
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDisposition,// CREATE_NEW, OPEN_EXISTING, etc.
    DWORD dwFlagsAndAttributes, // FILE_ATTRIBUTE_NORMAL
    HANDLE hTemplateFile
);

// Пример
HANDLE hFile = CreateFile(
    L"data.txt",
    GENERIC_READ | GENERIC_WRITE,
    0, NULL, OPEN_ALWAYS,
    FILE_ATTRIBUTE_NORMAL, NULL
);
Объекты ядра и их использование в приложении
Системное программирование

5.2. Чтение и запись файлов

// Чтение
BOOL ReadFile(
    HANDLE hFile,
    LPVOID lpBuffer,
    DWORD nNumberOfBytesToRead,
    LPDWORD lpNumberOfBytesRead,
    LPOVERLAPPED lpOverlapped
);

// Запись
BOOL WriteFile(
    HANDLE hFile,
    LPCVOID lpBuffer,
    DWORD nNumberOfBytesToWrite,
    LPDWORD lpNumberOfBytesWritten,
    LPOVERLAPPED lpOverlapped
);

// Закрытие
BOOL CloseHandle(HANDLE hFile);
Объекты ядра и их использование в приложении
Системное программирование

6. Мьютексы

6.1. Концепция мьютекса

Мьютекс (Mutex) — объект ядра для взаимного исключения.

Свойства:

  • Двоичный семафор (0 или 1)
  • Владение потоком
  • Межпроцессная синхронизация
  • Именованные объекты

Объекты ядра и их использование в приложении
Системное программирование

6.2. Использование мьютекса в Windows

// Создание
HANDLE hMutex = CreateMutex(
    NULL,       // Атрибуты безопасности
    FALSE,      // Начальное владение
    L"MyMutex"  // Имя (может быть NULL)
);

// Захват
DWORD result = WaitForSingleObject(hMutex, INFINITE);
if (result == WAIT_OBJECT_0) {
    // Критическая секция
}

// Освобождение
ReleaseMutex(hMutex);

// Закрытие
CloseHandle(hMutex);
Объекты ядра и их использование в приложении
Системное программирование

6.3. Мьютексы в POSIX

#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// Инициализация с атрибутами
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);

// Захват
pthread_mutex_lock(&mutex);

// Критическая секция

// Освобождение
pthread_mutex_unlock(&mutex);

// Уничтожение
pthread_mutex_destroy(&mutex);
Объекты ядра и их использование в приложении
Системное программирование

6.4. Типы мьютексов POSIX

Тип Описание
PTHREAD_MUTEX_NORMAL Обычный мьютекс
PTHREAD_MUTEX_RECURSIVE Рекурсивный (повторный захват)
PTHREAD_MUTEX_ERRORCHECK С проверкой ошибок
PTHREAD_MUTEX_ADAPTIVE Адаптивный (спинлок + блокировка)

Рекурсивный мьютекс:

pthread_mutex_lock(&mutex);   // Захват 1
pthread_mutex_lock(&mutex);   // Захват 2 (OK для RECURSIVE)
// ...
pthread_mutex_unlock(&mutex); // Освобождение 2
pthread_mutex_unlock(&mutex); // Освобождение 1
Объекты ядра и их использование в приложении
Системное программирование

7. Семафоры

7.1. Концепция семафора

Семафор — объект ядра с целочисленным счетчиком.

Операции Дейкстры:

  • P (Proberen) — уменьшить, захватить
  • V (Verhogen) — увеличить, освободить

Применение:

  • Ограничение числа потоков
  • Управление пулом ресурсов
  • Сигнализация между процессами
Семафор = N (максимум потоков)

Поток 1: P() → Семафор = N-1 → доступ
Поток 2: P() → Семафор = N-2 → доступ
...
Поток N: P() → Семафор = 0 → блокировка
Объекты ядра и их использование в приложении
Системное программирование

7.2. Семафоры в Windows

// Создание
HANDLE hSemaphore = CreateSemaphore(
    NULL,       // Атрибуты
    2,          // Начальное значение
    5,          // Максимальное значение
    L"MySem"    // Имя
);

// Захват (уменьшение)
DWORD result = WaitForSingleObject(hSemaphore, INFINITE);

// Освобождение (увеличение)
ReleaseSemaphore(hSemaphore, 1, NULL);

// Закрытие
CloseHandle(hSemaphore);
Объекты ядра и их использование в приложении
Системное программирование

7.3. Семафоры в POSIX

#include <semaphore.h>

// Именованный семафор
sem_t *sem = sem_open("/mysem", O_CREAT, 0644, 5);

// Безымянный семафор
sem_t sem;
sem_init(&sem, 0, 5);  // 0 — межпоточный, 5 — начальное значение

// Операции
sem_wait(&sem);     // P() — захват
sem_post(&sem);     // V() — освобождение

// Получение значения
int value;
sem_getvalue(&sem, &value);

// Закрытие
sem_close(sem);
sem_unlink("/mysem");
Объекты ядра и их использование в приложении
Системное программирование

7.4. Сравнение примитивов синхронизации

Примитив Windows POSIX Назначение
Мьютекс Mutex pthread_mutex_t Взаимное исключение
Семафор Semaphore sem_t Ограничение доступа
Событие Event condition variable Уведомление
Крит. секция Critical Section Быстрое исключение

Рекомендации:

  • Critical Section — для потоков одного процесса
  • Mutex — для межпроцессной синхронизации
  • Semaphore — для пулов ресурсов
Объекты ядра и их использование в приложении
Системное программирование

Резюме

Ключевые моменты лекции:

  1. Состояние гонки — проблема параллельного доступа к данным
  2. Критическая секция — код, требующий защиты
  3. Спинлоки — активное ожидание для коротких секций
  4. Мьютексы — взаимное исключение с владением
  5. Семафоры — управление доступом к ограниченным ресурсам
  6. Объекты ядра — мощный инструмент синхронизации
Объекты ядра и их использование в приложении
Системное программирование

Вопросы для самопроверки

  1. Что такое состояние гонки и как его избежать?
  2. Какие условия должны выполняться для корректной синхронизации?
  3. Чем отличается мьютекс от семафора?
  4. Какие проблемы возникают при принудительном завершении потока?
  5. Как работают атомарные операции?
  6. Когда использовать спинлоки, а когда мьютексы?
Объекты ядра и их использование в приложении
Системное программирование

Практические задания

Самостоятельная работа (8 часов):

  1. Написать программу с созданием процесса (CreateProcess/fork)
  2. Реализовать создание и завершение потока
  3. Использовать мьютекс для защиты общей переменной
  4. Написать программу с семафором для ограничения параллельных задач
Объекты ядра и их использование в приложении
Системное программирование

Рекомендуемая литература

Основная:

  1. Таненбаум, Э. Современные операционные системы. — 4-е изд. — СПб.: Питер, 2021.
  2. Kerrisk, M. The Linux Programming Interface. — No Starch Press, 2010.
  3. Бутенхоф, Д. Программирование с использованием POSIX Threads. — СПб.: Символ-Плюс, 2002.

Дополнительная:

  1. C++ Concurrency In Action (Williams, A.) — Manning, 2019
  2. man pages: pthread_mutex(3), sem_overview(7), CreateThread
  3. MSDN: Synchronization Objects
Объекты ядра и их использование в приложении

Кратко пробежаться по плану, акцентировать внимание на том, что сегодня переходим от теоретических основ к практическим средствам ОС. Напомнить, что тема связана с предыдущей лекцией о потоках и процессах. Данная лекция фокусируется на практическом использовании API объектов синхронизации. Теоретические основы — условия корректности, алгоритмы взаимного исключения, классические задачи — подробно рассматриваются в лекции 4.

Важно проговорить ассемблерный пример пошагово — показать, что counter++ не атомарная операция. Спросить студентов, какое максимальное значение может получиться при двух потоках, увеличивающих counter 1000 раз каждый.

Четыре условия корректности — классика (Таненбаум, Дейкстра), студенты должны знать наизусть. Обратить внимание на голодание — часто забывают. Спросить: как аппаратные средства отличаются от средств ОС по уровню абстракции?

Подчеркнуть, что test_and_set и CAS выполняются атомарно на уровне процессора благодаря блокировке шины или кэш-когерентности. Спросить: почему простое присваивание lock=1 не работает для взаимного исключения?

Показать, что спинлок годится только для очень коротких критических секций. Спросить: что будет с производительностью, если 100 потоков ждут на спинлоке? Упомянуть, что в ядре Linux часто используется адаптивный спинлок.

Важный переходный слайд — от низкоуровневых средств к высокоуровневым объектам ОС. Таблицу можно не зачитывать целиком, но подчеркнуть разницу между Signaled и Nonsignaled.

Обратить внимание на параметр bWaitAll в WaitForMultipleObjects — это частый источник ошибок. Спросить: в чём разница между INFINITE и 0? Упомянуть, что WaitForSingleObject возвращает WAIT_TIMEOUT, WAIT_FAILED и WAIT_ABANDONED.

Подчеркнуть: CreateProcess создаёт и процесс, и его первичный поток — это распространённое заблуждение. Показать, что CloseHandle для pi.hThread не завершает поток, а лишь закрывает дескриптор.

Акцентировать внимание на то, что CreateThread принимает void* — нужно приводить типы. Спросить: что произойдёт, если забыть вызвать CloseHandle? Утечка дескрипторов — частая ошибка на экзамене.

Ключевой момент — TerminateThread/Process это last resort. Спросить: какие именно ресурсы не освободятся? (куча, мьютексы, файлы). Упомянуть, что в Unix SIGKILL аналогичен TerminateProcess.

CreateFile — обобщённая функция: работает с файлами, pipe, консолью, COM-портами. Обратить внимание на параметр dwShareMode — студенты часто ставят 0 и не понимают, почему другой процесс не может открыть файл.

Подчеркнуть, что ReadFile может прочитать меньше байт, чем запрошено — всегда проверять lpNumberOfBytesRead. Упомянуть, что CloseHandle в Windows закрывает дескрипторы файлов, потоков, процессов, мьютексов — единый механизм.

Ключевое отличие мьютекса от семафора — владение. Спросить: что будет, если поток освободит мьютекс, который не захватывал? (undefined behaviour в POSIX, ошибка в Windows).

Показать типичный паттерн: WaitForSingleObject + работа + ReleaseMutex. Спросить: что будет, если забыть ReleaseMutex? Ответ: deadlock для других потоков. Упомянуть WAIT_ABANDONED — ситуация, когда владелец мьютекса завершился без освобождения.

Обратить внимание на PTHREAD_MUTEX_INITIALIZER — для статической инициализации без вызова pthread_mutex_init. Спросить: зачем нужен рекурсивный мьютекс? (рефакторинг с вложенными вызовами, рекурсивные функции).

Адаптивный мьютекс — интересная тема для углублённого обсуждения: сначала спинит, потом блокируется. На экзамене часто спрашивают разницу между NORMAL и ERRORCHECK.

Напомнить, что Дейкстра предложил семафоры в 1965 году. Спросить: при каком значении счётчика семафор превращается в мьютекс? (начальное = 1, максимальное = 1). Подчеркнуть, что семафор не имеет понятия владения.

Показать, что ReleaseSemaphore может увеличивать счётчик на любое значение (не только на 1). Частый вопрос: что произойдёт, если ReleaseSemaphore увеличит счётчик сверх максимума? (ошибка, функция вернёт FALSE).

Важно различать именованные и безымянные семафоры. Безымянные — только для потоков одного процесса через общую память. sem_unlink удаляет имя из файловой системы — без этого именованный семафор «висит» после закрытия.

Сводная таблица — хороший момент для интерактива: спросить студентов, какой примитив выбрать в конкретных ситуациях. Подчеркнуть, что Critical Section — только Windows и только внутрипроцессно.

Пробежаться по ключевым моментам, дать студентам время записать. Спросить, какой пункт вызвал больше всего вопросов — разобрать ещё раз.

Вопросы 3 и 6 — наиболее вероятные темы для экзамена. Рекомендовать студентам ответить на все вопросы вслух в парах.

Задание 3 — самое важное, обязательное для всех. Задание 4 — повышенной сложности для сильных студентов. Напомнить дедлайн и формат сдачи.

Книгу Керриска (TLPI) можно найти онлайн. man pages — лучший справочник для POSIX. MSDN — для Windows API. Напомнить, что на следующей лекции будут задачи для контрольной работы.